/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * This class provides support for system-level signals. <br/>
 * This implementation is based on the following classes:
 * <ul>
 *   <li><tt>sun.misc.Signal</tt>;</li>
 *   <li><tt>sun.misc.SignalHandler</tt>.</li>
 * </ul>
 * If theses classes are not present (which may happen, as theses classes are
 * not part of every JVM), methods of this class silently fail. <br/>
 * Availability of these classes can be tested using {@link #isAvailable()}.
 * <br/><br/>
 * This class uses reflection and proxy, and hence has no direct dependency
 * towards the two classes listed above. This allows both compilation with and
 * execution in a JVM lacking these classes.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
public final class Signal {

    /** Signal number for invalid signal. */
    public static final int INVALID_SIGNAL_NUMBER = -1;

    /** Signal name for invalid signal. */
    public static final String INVALID_SIGNAL_NAME = "invalid-signal";

    /** Value of default signal handler. */
    public static final Handler DEFAULT_HANDLER;

    /** Value of <i>ignore</i> signal handler. */
    public static final Handler IGNORE_HANDLER;

    /**
     * Represents the different kinds of signals that a program may handle.<br/>
     * There is no <i>strong</i> guarantee that the integer identifier of
     * a <i>kind</i> is equal to the signal number of the signal having the
     * same name on a given system. However, such a property should hold on
     * most systems. <br/>
     * <br/>
     * It has been chosen to differentiate name and string identifier in this
     * enum to allow that a (symbolic) name change in this enum do not break
     * the link with <tt>sun.misc.Signal</tt>.
     */
    public static enum Kind {

        /** Signal constant (terminal line hangup). */
        HUP("HUP", 1),

            /** Signal constant (interrupt program). */
            INT("INT", 2),

            /** Signal constant (quit program). */
            QUIT("QUIT", 3),

            /** Signal constant (illegal instruction). */
            ILL("ILL", 4),

            /** Signal constant (trace trap). */
            TRAP("TRAP", 5),

            /** Signal constant (abort program). */
            ABRT("ABRT", 6),

            /** Signal constant (emulate instruction executed). */
            EMT("EMT", 7),

            /** Signal constant (floating-point exception). */
            FPE("FPE", 8),

            /** Signal constant (kill program). */
            KILL("KILL", 9),

            /** Signal constant (bus error). */
            BUS("BUS", 10),

            /** Signal constant (segmentation violation). */
            SEGV("SEGV", 11),

            /** Signal constant (non-existent system call invoked). */
            SYS("SYS", 12),

            /** Signal constant (write on a pipe with no reader). */
            PIPE("PIPE", 13),

            /** Signal constant (real-time timer expired). */
            ALRM("ALRM", 14),

            /** Signal constant (software termination signal). */
            TERM("TERM", 15),

            /** Signal constant (urgent condition present on socket). */
            URG("URG", 16),

            /** Signal constant (stop - cannot be caught or ignored). */
            STOP("STOP", 17),

            /** Signal constant (stop signal generated from keyboard). */
            TSTP("TSTP", 18),

            /** Signal constant (continue after stop). */
            CONT("CONT", 19),

            /** Signal constant (child status has changed). */
            CHLD("CHLD", 20),

            /** Signal constant (background read attempted from control terminal). */
            TTIN("TTIN", 21),

            /** Signal constant (background write attempted to control terminal). */
            TTOU("TTOU", 22),

            /** Signal constant (I/O is possible on a descriptor). */
            IO("IO", 23),

            /** Signal constant (CPU time limit exceeded). */
            XCPU("XCPU", 24),

            /** Signal constant (file size limit exceeded). */
            XFSZ("XFSZ", 25),

            /** Signal constant (virtual time alarm). */
            VTALRM("VTALRM", 26),

            /** Signal constant (profiling timer alarm). */
            PROF("PROF", 27),

            /** Signal constant (window size change). */
            WINCH("WINCH", 28),

            /** Signal constant (status request from keyboard). */
            INFO("INFO", 29),

            /** Signal constant (user defined signal 1). */
            USR1("USR1", 30),

            /** Signal constant (user defined signal 2). */
            USR2("USR2", 31),

            /** Signal constant (thread interrupt). */
            THR("THR", 32);

        /** String identifier of signal. */
        private final String id;

        /** Integer identifier of signal. */
        private final int no;

        /**
         * Constructs a signal from string and integer identifiers.
         * @param s string identifier - should not be <tt>null</tt>
         * @param n integer identifier - should be > 0
         */
        private Kind(final String s, final int n) {
            assert s != null : "null s";
            assert n > 0 : "n should be > 0";
            this.id = s;
            this.no = n;
        } // end constructor(String, int)

        /**
         * Returns the string identifier of the signal.
         * @return the string identifier of the signal
         */
        public String getID() {
            return this.id;
        } // end method 'getID()'

        /**
         * Returns the integer identifier of the signal.
         * @return the integer identifier of the signal
         */
        public int getNo() {
            return this.no;
        } // end method 'getNo()'

    } // end enum 'Kind'

    /** Fully-qualified name of signal class. */
    private static final String CLASS_SIGNAL = "sun.misc.Signal";

    /** Name of method getting name from a signal. */
    private static final String METH_SIGNAL_GET_NAME = "getName";

    /** Name of method getting number from a signal. */
    private static final String METH_SIGNAL_GET_NUMBER = "getNumber";

    /** Name of method setting handler for a signal (static). */
    private static final String METH_SIGNAL_HANDLE = "handle";

    /** Name of method raising a signal (static). */
    private static final String METH_SIGNAL_RAISE = "raise";

    /** Fully-qualified name of signal-handler class. */
    private static final String CLASS_HANDLER = "sun.misc.SignalHandler";

    /** Name of method actually handling a signal. */
    private static final String METH_HANDLER_HANDLE = "handle";

    /** Name of default signal handler (static field). */
    private static final String FLD_HANDLER_DEFAULT = "SIG_DFL";

    /** Name of <i>ignore</i> signal handler (static field). */
    private static final String FLD_HANDLER_IGNORE = "SIG_IGN";

    /**
     * Whether signals are available from this class using
     * <tt>sun.misc</tt> classes.
     */
    private static final boolean AVAILABLE;

    /** Reference to signal class. */
    private static final Class CL_SIGNAL;

    /** Reference to handler class. */
    private static final Class CL_HANDLER;

    /** Reference to signal constructor. */
    private static final Constructor CSTR_SIGNAL;

    /** Reference to method getting name from a signal. */
    private static final Method MTH_SIGNAL_GET_NAME;

    /** Reference to method getting number from a signal. */
    private static final Method MTH_SIGNAL_GET_NUMBER;

    /** Reference to method setting handler for a signal (static). */
    private static final Method MTH_SIGNAL_HANDLE;

    /** Reference to method raising a signal (static). */
    private static final Method MTH_SIGNAL_RAISE;

    /** Reference to method actually handling a signal. */
    private static final Method MTH_HANDLER_HANDLE;

    /** Reference to underlying <tt>sun.misc.Signal</tt> object. */
    private final Object inst;

    /*
     * Initializes the references to classes, constructors, methods,
     * and values, using reflection.
     */
    static {
        // elements are temporarily stored in local variables to allow
        // the fields to be <tt>final</tt>
        Class<?> clSignal;
        Class<?> clHandler;
        Constructor cstrSignal;
        Method methSignalGetName;
        Method methSignalGetNumber;
        Method methSignalHandle;
        Method methSignalRaise;
        Method methHandlerHandle;
        Object defaultHandler;
        Object ignoreHandler;
        boolean available;
        try {
            clSignal = Class.forName(Signal.CLASS_SIGNAL);
            clHandler = Class.forName(Signal.CLASS_HANDLER);
            cstrSignal = clSignal.getConstructor(String.class);
            methSignalGetName =
                clSignal.getMethod(Signal.METH_SIGNAL_GET_NAME);
            methSignalGetNumber =
                clSignal.getMethod(Signal.METH_SIGNAL_GET_NUMBER);
            methSignalHandle = clSignal.getMethod(Signal.METH_SIGNAL_HANDLE,
                                                    clSignal,
                                                    clHandler);
            methSignalRaise =
                clSignal.getMethod(Signal.METH_SIGNAL_RAISE, clSignal);
            methHandlerHandle =
                clHandler.getMethod(Signal.METH_HANDLER_HANDLE,
                                     clSignal);
            defaultHandler =
                clHandler.getField(Signal.FLD_HANDLER_DEFAULT).get(null);
            ignoreHandler =
                clHandler.getField(Signal.FLD_HANDLER_IGNORE).get(null);
            available = true;
        } catch (final Throwable t) {
            clSignal = null;
            clHandler = null;
            cstrSignal = null;
            methSignalGetName = null;
            methSignalGetNumber = null;
            methSignalHandle = null;
            methSignalRaise = null;
            methHandlerHandle = null;
            defaultHandler = null;
            ignoreHandler = null;
            available = false;
        } // end try/catch
        CL_SIGNAL = clSignal;
        CL_HANDLER = clHandler;
        CSTR_SIGNAL = cstrSignal;
        MTH_SIGNAL_GET_NAME = methSignalGetName;
        MTH_SIGNAL_GET_NUMBER = methSignalGetNumber;
        MTH_SIGNAL_HANDLE = methSignalHandle;
        MTH_SIGNAL_RAISE = methSignalRaise;
        MTH_HANDLER_HANDLE = methHandlerHandle;
        DEFAULT_HANDLER = new SystemHandler(defaultHandler);
        IGNORE_HANDLER = new SystemHandler(ignoreHandler);
        AVAILABLE = available;
    } // end static block

    /**
     * Constructs a signal from a kind. <br/>
     * One may use {@link #isValid()} to check whether the constructed signal
     * is both valid and supported by the underlying system.
     * @param k kind of signal - should not be <tt>null</tt>
     */
    public Signal(final Kind k) {
        assert k != null : "null k";
        Object obj;
        if (isAvailable()) {
            try {
                obj = Signal.CSTR_SIGNAL.newInstance(k.getID());
            } catch (final Throwable t) {
                obj = null;
            } // end try/catch
        } else {
            obj = null;
        } // end if/else
        this.inst = obj;
    } // end constructor(Kind)

    /**
     * Constructs a signal from a <tt>sun.misc.Signal</tt>.
     * @param signal underlying signal - should be an instance of
     *               <tt>sun.misc.Signal</tt>
     */
    private Signal(final Object signal) {
        assert signal.getClass().getName().equals(Signal.CLASS_SIGNAL)
            : "invalid signal";
        if (isAvailable()) {
            this.inst = signal;
        } else {
            this.inst = null;
        } // end if/else
    } // end constructor(Object)

    /**
     * Tests whether signal processing is available.
     * @return <tt>true</tt> if signal processing is available,
     *         <tt>false</tt> otherwise
     */
    public static boolean isAvailable() {
        return Signal.AVAILABLE;
    } // end method 'isAvailable()'

    /**
     * Checks whether the signal is valid and can thus be used.
     * @return <tt>true</tt> if the signal is valid, <tt>false</tt> otherwise
     */
    public boolean isValid() {
        return this.inst != null;
    } // end method 'isValid()'

    /**
     * Returns the signal number.
     * @return the signal number,
     *         or <tt>INVALID_SIGNAL_NUMBER</tt> if either an error occurs
     *         or signal classes are lacking or signal is invalid
     * @see #INVALID_SIGNAL_NUMBER
     */
    public int getNumber() {
        if (isValid()) {
            try {
                return
                    (Integer) Signal.MTH_SIGNAL_GET_NUMBER.invoke(this.inst);
            } catch (final Throwable t) {
                return Signal.INVALID_SIGNAL_NUMBER;
            } // end try/catch
        } else {
            return Signal.INVALID_SIGNAL_NUMBER;
        } // end if/else
    } // end method 'getNumber()'

    /**
     * Returns the signal name.
     * @return the signal name,
     *         or <tt>INVALID_SIGNAL_NAME</tt> if either an error occurs
     *         or signal classes are lacking or signal is invalid
     * @see #INVALID_SIGNAL_NAME
     */
    public String getName() {
        if (isValid()) {
            try {
                return (String) Signal.MTH_SIGNAL_GET_NAME.invoke(this.inst);
            } catch (final Throwable t) {
                return Signal.INVALID_SIGNAL_NAME;
            } // end try/catch
        } else {
            return Signal.INVALID_SIGNAL_NAME;
        } // end if/else
    } // end method 'getName()'

    /**
     * Raises the signal.
     * @return <tt>true</tt> if signals are supported, the current signal is
     *         valid, and no error occured, <tt>false</tt> otherwise
     */
    public boolean raise() {
        if (isValid()) {
            try {
                Signal.MTH_SIGNAL_RAISE.invoke(null, this.inst);
                return true;
            } catch (final Throwable t) {
                return false;
            } // end try/catch
        } else {
            return false;
        } // end if/else
    } // end method 'raise()'

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append(getName());
        sb.append('/');
        sb.append(getNumber());
        return sb.toString();
    } // end method 'toString()'

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj instanceof Signal) {
            final Signal that = (Signal) obj;
            return this.getName().equals(that.getName())
                && this.getNumber() == that.getNumber();
        } else {
            return false;
        } // end if/else
    } // end method 'equals(Object)'

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return getNumber() + getName().hashCode();
    } // end method 'hashCode()'

    /**
     * Registers a signal handler for a list of signals.
     * @param handler signal handler - should not be <tt>null</tt>
     * @param signals list of signals <br/>
     *        invalid signals are ignored
     * @return <tt>true</tt> if signals are supported and no error occured,
     *         <tt>false</tt> otherwise
     */
    public static boolean handle(final Handler handler,
                                 final Signal... signals) {
        assert handler != null : "null handler";
        assert signals != null : "null signals";
        if (isAvailable()) {
            try {
                if (handler instanceof SystemHandler) {
                    final Object actualHandler =
                        ((SystemHandler) handler).getActualHandler();
                    try {
                        for (Signal s : signals) {
                            if (s.isValid()) {
                                Signal.MTH_SIGNAL_HANDLE.invoke(null,
                                                                s.inst,
                                                                actualHandler);
                            } // end if
                        } // end for
                        return true;
                    } catch (final Throwable t) {
                        return false;
                    } // end try/catch
                } else {
                    final Object h =
                        Proxy.newProxyInstance(Signal.class.getClassLoader(),
                                               new Class[] {Signal.CL_HANDLER},
                                               new ProxyHandler(handler));
                    for (Signal s : signals) {
                        if (s.isValid()) {
                            Signal.MTH_SIGNAL_HANDLE.invoke(null, s.inst, h);
                        } // end if
                    } // end for
                    return true;
                } // end if/else
            } catch (final Throwable t) {
                return false;
            } // end try/catch
        } else {
            return false;
        } // end if/else
    } // end method 'handle(Handler, Signal...)'

    /**
     * This interface describes a signal handler.
     */
    public interface Handler {

        /**
         * Called when a registered signal should be treated.
         * @param signal signal to treat
         */
        void handle(Signal signal);

    } // end inner-interface 'Handler'

    /**
     * This class implements a bridge from "native"
     * (<tt>sun.misc.SignalHandler</tt>)
     * handler to <tt>Signal.Handler</tt>.
     */
    private static final class SystemHandler implements Signal.Handler {

        /** Wrapped instance. */
        private final Object actualHandler;

        /**
         * Constructs an instance from a "native" handler.
         * @param h "native" handler to wrap - should not be <tt>null</tt>
         */
        private SystemHandler(final Object h) {
            assert h != null : "null h";
            this.actualHandler = h;
        } // end constructor(Object)

        /**
         * Returns the wrapped handler.
         * @return the wrapped handler
         */
        private Object getActualHandler() {
            return this.actualHandler;
        } // end method 'getActualHandler()'

        /**
         * Placeholder - never called as <tt>sun.misc</tt> signals are
         * directly registered and called.
         * @param signal ignored
         */
        public void handle(final Signal signal) {
            // never called ...
        } // end method 'handle(Signal)'

    } // end inner-interface 'SystemHandler'

    /**
     * This class implements a proxy that acts as a bridge from
     * <tt>sun.misc.SignalHandler</tt> to
     * <tt>fr.x9c.cadmium.util.Signal.Handler</tt>.
     */
    private static final class ProxyHandler implements InvocationHandler {

        /** Handler to forward calls to. */
        private final Handler handler;

        /**
         * Constructs an instance from a handler.
         * @param h handler to forward calls to - should not be <tt>null</tt>
         */
        private ProxyHandler(final Handler h) {
            assert h != null : "null h";
            this.handler = h;
        } // end constructor(Handler)

        /**
         * {@inheritDoc}
         */
        @Override
        public Object invoke(final Object o, final Method m, final Object[] a)
            throws Throwable {
            // no test over 'm' as sun.misc.SignalHandler has only 1 method
            // (this method accepting 1 parameter)
            this.handler.handle(new Signal(a[0]));
            return null;
        } // end method 'invoke(Object, Method, Object[])'

    } // end inner-class 'ProxyHandler'

} // end class 'Signal'
